package cn.com.commons;

import java.io.Serializable;

/**
 * 分页器，此类对于在web页面上分页显示多项内容，计算页码和当前页的偏移量，十分方便实用。
 * 该类只需要知道总共有多少项，当前显示每几页，每页显示几项，就可以帮你计算出其它数据，而且保证所有计算都得出合理的值，不用担心页码超出边界之类的问题。
 * <p>
 * <strong>使用方法如下:</strong>
 * 
 * <pre>
 * // 创建一个翻页器，可以在此指定每页显示几项，也可以以后再指定。
 * // 如果没有指定，则使用默认值每页最多显示10项。
 * Paginator&lt;List, Map&gt; pg = new Paginator&lt;List, Map&gt;();
 * 
 * // 告诉我总共有几项。如果给的数字小于0，就看作0。
 * pg.setTotalItems(123);
 * // 如果不知道有几项，可以这样。
 * // pg.setTotalItems(Paginator.UNKNOWN_ITEMS);
 * System.out.println(pg);
 * 
 * // 现在默认当前页是第一页，但你可以改变它。
 * pg.setCurrentPage(3);// 这样当前页就是3了，不用担心页数会超过总页数。
 * System.out.println(pg);
 * 
 * // 现在你可以得到各种数据了。
 * int currentPage = pg.getCurrentPage(); // 得到当前页
 * int totalPages = pg.getTotalPages(); // 总共有几页
 * int totalItems = pg.getTotalItems(); // 总共有几项
 * int beginIndex = pg.getBeginIndex(); // 得到当前页第一项的序号(从1开始数的)
 * int endIndex = pg.getEndIndex(); // 得到当前页最后一项的序号(也是从1开始数)
 * int offset = pg.getOffset(); // offset和length可以作为mysql查询语句
 * int length = pg.getLength(); // 的limit offset，length子句。
 * 
 * // 还可以做调整。
 * pg.setPageSize(20); // 这样就每页显示20个了，当前页的值会自动调整,并使新页和原来的页显示同样的项，这样用户不容易迷惑。
 * pg.setItem(33); // 这样可以把页码调整到显示第33号项(从0开始计数)的那一页
 * System.out.println(pg);
 * 
 * // 高级功能，开一个滑动窗口。我们经常要在web页面上显示一串的相邻页码，供用户选择。
 * // ____________________________________________________________
 * // 例如: &lt;&lt; &lt; 3 4 5 6 [7] 8 9 &gt; &gt;&gt;
 * // 以上例子就是一个大小为7的滑动窗口，当前页码被尽可能摆在中间，除非当前页位于开头或结尾。
 * // 使用下面的调用，就可以得到指定大小的滑动窗口中的页码数组。
 * int[] slider = pg.getSlider();
 * String show = &quot;&quot;;
 * // 这样可以判断指定页码是否有效，或者是当前页。无效的页码在web页面上不需要链接。
 * int i = 0;
 * if (pg.isDisabled(slider[i])) {
 * 	show = &quot;this is page &quot; + slider[i] + &quot; &quot;;
 * } else {
 * 	show = &quot;&lt;a href='#'&gt; page &quot; + slider[i] + &quot; &lt;/a&gt;&quot;;
 * }
 * // 可以直接打印出pg，用于调试程序。
 * System.out.println(pg);
 * System.out.println(show);
 * </pre>
 * 
 * @param <C>
 *            查询条件类型
 * @param <R>
 *            结果数据集类型
 * @author Microbun 2012-2-9下午10:38:57
 * @version $Id: Paginator.java 134 2012-03-27 13:18:14Z Microbun $
 * @since 1.0
 */
public class Paginator<C, R> implements Serializable, Cloneable {

	/**
	 * @Field serialVersionUID:TODO
	 */
	private static final long serialVersionUID = -3819519212415736022L;

	/** 每页默认的项数(10) */
	public static final int DEFAULT_PAGE_SIZE = 10;

	/** 滑动窗口默认的大小(7) */
	public static final int DEFAULT_SLIDER_SIZE = 7;

	/** 表示项数未知(<code>Integer.MAX_VALUE</code>)。 */
	public static final int UNKNOWN_ITEMS = Integer.MAX_VALUE;

	/* 数据值 */
	private C condition;// 查询条件集
	private R result;// 查询结构集

	/* 状态值 */
	private int currentPage; // 当前页码。(1-based)
	private int totalItems; // 总共项数
	private int pageSize; // 每页项数。

	/**
	 * 创建一个分页器，初始项数为无限大<code>UNKNOWN_ITEMS</code>，默认每页显示<code>10</code>项。
	 */
	public Paginator() {
		this(0);
	}

	/**
	 * 创建一个分页器，初始项数为无限大<code>UNKNOWN_ITEMS</code>，指定每页项数。
	 * 
	 * @param pageSize
	 *            每页项数。
	 */
	public Paginator(int pageSize) {
		this(pageSize, UNKNOWN_ITEMS);
	}

	/**
	 * 创建一个分页器，初始项数为无限大<code>UNKNOWN_ITEMS</code>，指定每页项数。
	 * 
	 * @param pageSize
	 *            页面大小
	 * @param totalItems
	 *            总项数
	 */
	public Paginator(int pageSize, int totalItems) {
		this.totalItems = (totalItems >= 0) ? totalItems : 0;
		this.pageSize = (pageSize > 0) ? pageSize : DEFAULT_PAGE_SIZE;
		this.currentPage = calcPage(0);
	}

	/**
	 * 获取查询条件
	 * 
	 * @return <code>C<code> 查询条件对象
	 */
	public C getCondition() {
		return condition;
	}

	/**
	 * 获取查询条件
	 * 
	 * @param query
	 *            查询条件对象
	 */
	public void setCondition(C condition) {
		this.condition = condition;
	}

	/**
	 * 获取查询结果
	 * 
	 * @return <code>R<code> 查询结果集
	 */
	public R getResult() {
		return result;
	}

	/**
	 * 设置查询结果集
	 * 
	 * @param result
	 *            查询结果集
	 */
	public void setResult(R result) {
		this.result = result;
	}

	/**
	 * 取得总页数。
	 * 
	 * @return <code>int</code> 总页数
	 */
	public int getTotalPages() {
		return (int) Math.ceil((double) totalItems / pageSize);
	}

	/**
	 * 取得当前页。
	 * 
	 * @return <code>int</code> 当前页
	 */
	public int getCurrentPage() {
		return currentPage;
	}

	/**
	 * 设置并取得当前页。实际的当前页值被确保在正确的范围内。
	 * 
	 * @param currentPage
	 *            当前页
	 * 
	 * @return <code>int</code>设置后的当前页
	 */
	public int setCurrentPage(int currentPage) {
		return (this.currentPage = calcPage(currentPage));
	}

	/**
	 * 取得总项数。
	 * 
	 * @return <code>int</code>总项数
	 */
	public int getTotalItems() {
		return totalItems;
	}

	/**
	 * 设置并取得总项数。如果指定的总项数小于0，则被看作0。自动调整当前页，确保当前页值在正确的范围内。
	 * 
	 * @param totalItems
	 *            总项数
	 * 
	 * @return <code>int</code>设置以后的总项数
	 */
	public int setTotalItems(int totalItems) {
		this.totalItems = (totalItems >= 0) ? totalItems : 0;
		setCurrentPage(currentPage);
		return this.totalItems;
	}

	/**
	 * 取得每页项数。
	 * 
	 * @return <code>int</code>每页项数
	 */
	public int getPageSize() {
		return pageSize;
	}

	/**
	 * 设置并取得每页项数。如果指定的每页项数小于等于0，则使用默认值<code>DEFAULT_PAGE_SIZE</code>。
	 * 并调整当前页使之在改变每页项数前后显示相同的项。
	 * 
	 * @param pageSize
	 *            每页项数
	 * 
	 * @return 设置后的每页项数
	 */
	public int setPageSize(int pageSize) {
		int tmp = this.pageSize;
		this.pageSize = (pageSize > 0) ? pageSize : DEFAULT_PAGE_SIZE;
		if (currentPage > 0) {
			setCurrentPage((int) (((double) (currentPage - 1) * tmp) / this.pageSize) + 1);
		}
		return this.pageSize;
	}

	/**
	 * 取得当前页第一项在全部项中的偏移量 (0-based)。
	 * 
	 * @return 偏移量
	 */
	public int getOffset() {
		return (currentPage > 0) ? (pageSize * (currentPage - 1)) : 0;
	}

	/**
	 * 取得当前页的长度，即当前页的实际项数。相当于 <code>endIndex() - beginIndex() + 1</code>
	 * 
	 * @return 当前页的长度
	 */
	public int getLength() {
		if (currentPage > 0) {
			return Math.min(pageSize * currentPage, totalItems)
					- (pageSize * (currentPage - 1));
		} else {
			return 0;
		}
	}

	/**
	 * 取得当前页显示的项的起始序号 (1-based)。
	 * 
	 * @return 起始序号
	 */
	public int getBeginIndex() {
		if (currentPage > 0) {
			return (pageSize * (currentPage - 1)) + 1;
		} else {
			return 0;
		}
	}

	/**
	 * 取得当前页显示的末项序号 (1-based)。
	 * 
	 * @return 末项序号
	 */
	public int getEndIndex() {
		if (currentPage > 0) {
			return Math.min(pageSize * currentPage, totalItems);
		} else {
			return 0;
		}
	}

	/**
	 * 设置当前页，使之显示指定offset(0-based)的项。
	 * 
	 * @param itemOffset
	 *            要显示的项的偏移量(0-based)
	 * 
	 * @return 指定项所在的页
	 */
	public int setItem(int itemOffset) {
		return setCurrentPage((itemOffset / pageSize) + 1);
	}

	/**
	 * 取得首页页码。
	 * 
	 * @return 首页页码
	 */
	public int getFirstPage() {
		return calcPage(1);
	}

	/**
	 * 取得末页页码。
	 * 
	 * @return 末页页码
	 */
	public int getLastPage() {
		return calcPage(getTotalPages());
	}

	/**
	 * 取得前一页页码。
	 * 
	 * @return 前一页页码
	 */
	public int getPreviousPage() {
		return calcPage(currentPage - 1);
	}

	/**
	 * 取得前n页页码
	 * 
	 * @param n
	 *            前n页
	 * 
	 * @return 前n页页码
	 */
	public int getPreviousPage(int n) {
		return calcPage(currentPage - n);
	}

	/**
	 * 取得后一页页码。
	 * 
	 * @return 后一页页码
	 */
	public int getNextPage() {
		return calcPage(currentPage + 1);
	}

	/**
	 * 取得后n页页码。
	 * 
	 * @param n
	 *            后n面
	 * 
	 * @return （currentPage + n）页
	 */
	public int getNextPage(int n) {
		return calcPage(currentPage + n);
	}

	/**
	 * 判断指定页码是否被禁止，也就是说指定页码超出了范围或等于当前页码。
	 * 
	 * @param page
	 *            页码
	 * 
	 * @return boolean 是否为禁止的页码(<code>true</code>为禁止页码，<code>false</code>为可用页码)
	 */
	public boolean isDisabled(int page) {
		return ((page < 1) || (page > getTotalPages()) || (page == this.currentPage));
	}

	/**
	 * 取得默认大小(<code>DEFAULT_SLIDER_SIZE</code>)的页码滑动窗口，并将当前页尽可能地放在滑动窗口的中间部位。参见
	 * {@link #getSlider(int n)}。
	 * 
	 * @return 包含页码的数组
	 */
	public int[] getSlider() {
		return getSlider(DEFAULT_SLIDER_SIZE);
	}

	/**
	 * 取得指定大小的页码滑动窗口，并将当前页尽可能地放在滑动窗口的中间部位。例如: 总共有13页，当前页是第5页，取得一个大小为5的滑动窗口，将包括
	 * 3，4，5，6, 7这几个页码，第5页被放在中间。如果当前页是12，则返回页码为 9，10，11，12，13。
	 * 
	 * @param width
	 *            滑动窗口大小
	 * 
	 * @return 包含页码的数组，如果指定滑动窗口大小小于1或总页数为0，则返回空数组。
	 */
	public int[] getSlider(int width) {
		int pages = getTotalPages();

		if ((pages < 1) || (width < 1)) {
			return new int[0];
		} else {
			if (width > pages) {
				width = pages;
			}

			int[] slider = new int[width];
			int first = currentPage - ((width - 1) / 2);

			if (first < 1) {
				first = 1;
			}

			if (((first + width) - 1) > pages) {
				first = pages - width + 1;
			}

			for (int i = 0; i < width; i++) {
				slider[i] = first + i;
			}

			return slider;
		}
	}

	/**
	 * 计算页数，但不改变当前页。
	 * 
	 * @param page
	 *            页码
	 * 
	 * @return 返回正确的页码(保证不会出边界)
	 */
	protected int calcPage(int page) {
		int pages = getTotalPages();
		if (pages > 0) {
			return (page < 1) ? 1 : ((page > pages) ? pages : page);
		}
		return 0;
	}

	/**
	 * 复制分页器（浅复制）
	 * 
	 * @return 不包含<code>C</code>和<code>R</code>的分页器
	 */
	public Paginator<C, R> clone() {
		try {
			@SuppressWarnings("unchecked")
			Paginator<C, R> paginator = (Paginator<C, R>) super.clone();
			paginator.setCondition(null);
			paginator.setResult(null);
			return paginator;
		} catch (java.lang.CloneNotSupportedException e) {
			return null; // 不可能发生
		}
	}

	/**
	 * 转换成字符串表示。
	 * 
	 * @return 字符串表示。
	 */
	public String toString() {
		StringBuilder strbuilder = new StringBuilder();
		if (getResult() != null) {
			strbuilder = new StringBuilder(getResult().toString() + "\n");
		} else {
			strbuilder = new StringBuilder("[]\n");
		}
		strbuilder.append("Paginator:page");
		if (getTotalPages() < 1) {
			strbuilder.append(getCurrentPage());
		} else {
			int[] slider = getSlider();

			for (int i = 0; i < slider.length; i++) {
				if (isDisabled(slider[i])) {
					strbuilder.append('[').append(slider[i]).append(']');
				} else {
					strbuilder.append(slider[i]);
				}

				if (i < (slider.length - 1)) {
					strbuilder.append('\t');
				}
			}
		}

		strbuilder.append(" of ").append(getTotalPages()).append(",\n");
		strbuilder.append("	Showing items ").append(getBeginIndex())
				.append(" to ").append(getEndIndex()).append(" (total ")
				.append(getTotalItems()).append(" items), ");
		strbuilder.append("offset=").append(getOffset()).append(", length=")
				.append(getLength());

		return strbuilder.toString();
	}

}
